
const { toString } = Object.prototype;
const { getPrototypeOf } = Object;

const _global = (() => {
    /*eslint no-undef:0*/
    if (typeof globalThis !== "undefined") return globalThis;
    return typeof self !== "undefined" ? self : (typeof window !== 'undefined' ? window : global)
})();


const isContextDefined = (context) => !isUndefined(context) && context !== _global;


/**
 * 判断是否是的字符串（不包括字符串null与undefined）
 * @param {*} obj 
 * @returns {Boolean} 
 */
const isString = (obj) => {
    return toString.call(obj) === '[object String]' && obj !== 'null' && obj !== 'undefined';
}

/**
 * 判断是否是数字
 * @param {Object} obj 
 * @returns 
 */
const isNumber = (obj) => {
    return toString.call(obj) === '[object Number]' && !isNaN(obj);
}

/**
 * 判读是否是函数
 * @param {Object} obj 
 * @returns 
 */
const isFunction = (obj) => {
    return toString.call(obj) === '[object Function]';
}

/**
 * 判断是否是一个数组
 * @param {*} val The value to test
 * @returns {boolean} True if value is an Array, otherwise false
 */
const { isArray } = Array;

/**
 * 判断是否普通对象
 * @param {*} val The value to test
 * @returns {boolean} True if value is a plain Object, otherwise false
 */
const isPlainObject = (val) => {

    return toString.call(val) === '[object Object]';

}

/**
 * 判断是否是一个空对象
 * @param {*} val 
 * @returns 
 */
const isEmptyObject = (val) => {
    return JSON.stringify(val) === '{}';
}

/**
 * 判断是否是undefined，包括字符串'undefined'
 * @param {*} val 
 * @returns 
 */
const isUndefined = (val) => {
    return toString.call(val) === '[object Undefined]' || val === 'undefined';
}

/**
 * 判断是否是null,包括字符串'null'
 * @param {*} val 
 * @returns 
 */
const isNull = (val) => {
    return toString.call(val) === '[object Null]' || val === 'null';
}

/**
 * 校验是否是基本类型
 * @param {*} val 
 * @returns 
 */
const isPrimitive = (val) => {
    return (
        typeof val === 'string' ||
        typeof val === 'number' ||
        typeof val === 'symbol' ||
        typeof val === 'boolean'
    )
}

/**
 * 校验是否是合法date
 * @param {*} date 
 * @returns 
 */
const isValidDate = (date) => {
    return date instanceof Date && !isNaN(date.getTime())
}

/**
 * 遍历数组或对象并调用对应的处理函数，不会主动改变原数据
 * If `obj` is an Array callback will be called passing
 * the value, index, and complete array for each item.
 *
 * If 'obj' is an Object callback will be called passing
 * the value, key, and complete object for each property.
 *
 * @param {Object|Array} obj The object to iterate
 * @param {Function} fn The callback to invoke for each item
 * @param {Boolean} [allOwnKeys = false]
 * @returns {any}
 */
function forEach(obj, fn, { allOwnKeys = false } = {}) {
    // Don't bother if no value provided
    if (obj === null || typeof obj === 'undefined') {
        return;
    }

    let i;
    let l;

    // Force an array if not already something iterable
    if (typeof obj !== 'object') {
        /*eslint no-param-reassign:0*/
        obj = [obj];
    }

    if (isArray(obj)) {
        // Iterate over array values
        for (i = 0, l = obj.length; i < l; i++) {
            fn.call(null, obj[i], i, obj);
        }
    } else {
        // Iterate over object keys
        const keys = allOwnKeys ? Object.getOwnPropertyNames(obj) : Object.keys(obj);
        const len = keys.length;
        let key;

        for (i = 0; i < len; i++) {
            key = keys[i];
            fn.call(null, obj[key], key, obj);
        }
    }
}

function findKey(obj, key) {
    key = key.toLowerCase();
    const keys = Object.keys(obj);
    let i = keys.length;
    let _key;
    while (i-- > 0) {
        _key = keys[i];
        if (key === _key.toLowerCase()) {
            return _key;
        }
    }
    return null;
}


/**
 * Accepts varargs expecting each argument to be an object, then
 * immutably merges the properties of each object and returns result.
 *
 * When multiple objects contain the same key the later object in
 * the arguments list will take precedence.
 *
 * Example:
 *
 * ```js
 * var result = merge({foo: 123}, {foo: 456});
 * console.log(result.foo); // outputs 456
 * ```
 *
 * @param {Object} obj1 Object to merge
 *
 * @returns {Object} Result of all merge properties
 */
function merge(/* obj1, obj2, obj3, ... */) {
    const { caseless } = isContextDefined(this) && this || {};
    const result = {};
    const assignValue = (val, key) => {
        const targetKey = caseless && findKey(result, key) || key;
        if (isPlainObject(result[targetKey]) && isPlainObject(val)) {
            result[targetKey] = merge(result[targetKey], val);
        } else if (isPlainObject(val)) {
            result[targetKey] = merge({}, val);
        } else if (isArray(val)) {
            result[targetKey] = val.slice();
        } else {
            result[targetKey] = val;
        }
    }

    for (let i = 0, l = arguments.length; i < l; i++) {
        arguments[i] && forEach(arguments[i], assignValue);
    }
    return result;
}

export default {
    isArray,
    isPlainObject,
    isEmptyObject,
    isUndefined,
    isNull,
    isString,
    isNumber,
    isFunction,
    isValidDate,
    forEach,
    isPrimitive,
    merge
}
